Micron Document
🌎 rns.kin.earth git 🌍

Node / dev / reticulum / commits / a11f14e

Commit a11f14e75fee0141fca2c4e4da9962a5cb046003


Parents : c32086c
Author : Mark Qvist <mark@unsigned.io>
Date : 2024-09-04T17:37:18+02:00

Implemented ratchets

Changes

6 files changed, 170 insertions(+), 48 deletions(-)

M RNS/Identity.py +135 -28
M RNS/Link.py +1 -1
M RNS/Packet.py +14 -5

Diff

diff --git a/RNS/Destination.py b/RNS/Destination.py
index 736dcba3..92e47359 100755
--- a/RNS/Destination.py
+++ b/RNS/Destination.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -72,7 +72,7 @@ class Destination:
directions = [IN, OUT]
PR_TAG_WINDOW = 30
- RATCHET_COUNT = 128
+ RATCHET_COUNT = 256
@staticmethod
def expand_name(identity, app_name, *aspects):
@@ -219,7 +219,7 @@ class Destination:
def rotate_ratchets(self):
if self.ratchets != None:
RNS.log("Rotating ratchets for "+str(self), RNS.LOG_DEBUG) # TODO: Remove
- new_ratchet = RNS.Identity.generate_ratchet()
+ new_ratchet = RNS.Identity._generate_ratchet()
self.ratchets.insert(0, new_ratchet)
if len (self.ratchets) > Destination.RATCHET_COUNT:
self.ratchets = self.ratchets[:Destination.RATCHET_COUNT]
@@ -241,6 +241,7 @@ class Destination:
if self.direction != Destination.IN:
raise TypeError("Only IN destination types can be announced")
+ ratchet = b""
now = time.time()
stale_responses = []
for entry_tag in self.path_responses:
@@ -269,8 +270,8 @@ class Destination:
if self.ratchets != None:
self.rotate_ratchets()
- ratchet_pub = RNS.Identity.ratchet_public_bytes(self.ratchets[0])
- RNS.log(f"Including {len(ratchet_pub)*8}-bit ratchet {RNS.hexrep(ratchet_pub)} in announce", RNS.LOG_DEBUG) # TODO: Remove
+ ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0])
+ RNS.log(f"Including {len(ratchet)*8}-bit ratchet {RNS.hexrep(ratchet)} in announce", RNS.LOG_DEBUG) # TODO: Remove
if app_data == None and self.default_app_data != None:
if isinstance(self.default_app_data, bytes):
@@ -280,13 +281,12 @@ class Destination:
if isinstance(returned_app_data, bytes):
app_data = returned_app_data
- signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash
+ signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
if app_data != None:
signed_data += app_data
signature = self.identity.sign(signed_data)
-
- announce_data = self.identity.get_public_key()+self.name_hash+random_hash+signature
+ announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
if app_data != None:
announce_data += app_data
@@ -298,8 +298,13 @@ class Destination:
else:
announce_context = RNS.Packet.NONE
- announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context, attached_interface = attached_interface)
+ if ratchet:
+ context_flag = RNS.Packet.FLAG_SET
+ else:
+ context_flag = RNS.Packet.FLAG_UNSET
+ announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
+ attached_interface = attached_interface, context_flag=context_flag)
if send:
announce_packet.send()
else:
@@ -485,7 +490,7 @@ class Destination:
return plaintext
if self.type == Destination.SINGLE and self.identity != None:
- return self.identity.encrypt(plaintext)
+ return self.identity.encrypt(plaintext, ratchet=RNS.Identity.get_ratchet(self.hash))
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
@@ -510,7 +515,7 @@ class Destination:
return ciphertext
if self.type == Destination.SINGLE and self.identity != None:
- return self.identity.decrypt(ciphertext)
+ return self.identity.decrypt(ciphertext, ratchets=self.ratchets)
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:

diff --git a/RNS/Identity.py b/RNS/Identity.py
index 010c1fd2..c7786634 100644
--- a/RNS/Identity.py
+++ b/RNS/Identity.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -50,7 +50,10 @@ class Identity:
KEYSIZE = 256*2
"""
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
- """
+ """
+
+ RATCHETSIZE = 256
+ RATCHET_EXPIRY = 60*60*24*30
# Non-configurable constants
FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD
@@ -67,6 +70,7 @@ class Identity:
# Storage
known_destinations = {}
+ known_ratchets = {}
@staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None):
@@ -222,29 +226,102 @@ class Identity:
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
@staticmethod
- def generate_ratchet():
+ def _ratchet_public_bytes(ratchet):
+ return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
+
+ @staticmethod
+ def _generate_ratchet():
ratchet_prv = X25519PrivateKey.generate()
ratchet_pub = ratchet_prv.public_key()
return ratchet_prv.private_bytes()
@staticmethod
- def ratchet_public_bytes(ratchet):
- return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
+ def _remember_ratchet(destination_hash, ratchet):
+ RNS.log(f"Remembering ratchet {RNS.hexrep(ratchet)} for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG) # TODO: Remove
+ try:
+ Identity.known_ratchets[destination_hash] = ratchet
+ hexhash = RNS.hexrep(destination_hash, delimit=False)
+ ratchet_data = {"ratchet": ratchet, "received": time.time()}
+
+ ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
+
+ if not os.path.isdir(ratchetdir):
+ os.makedirs(ratchetdir)
+
+ outpath = f"{ratchetdir}/{hexhash}.out"
+ finalpath = f"{ratchetdir}/{hexhash}"
+ ratchet_file = open(outpath, "wb")
+ ratchet_file.write(umsgpack.packb(ratchet_data))
+ ratchet_file.close()
+ os.rename(outpath, finalpath)
+
+ except Exception as e:
+ RNS.log(f"Could not persist ratchet for {RNS.prettyhexrep(destination_hash)} to storage.", RNS.LOG_ERROR)
+ RNS.log(f"The contained exception was: {e}")
+ RNS.trace_exception(e)
+
+ @staticmethod
+ def get_ratchet(destination_hash):
+ if not destination_hash in Identity.known_ratchets:
+ RNS.log(f"Trying to load ratchet for {RNS.prettyhexrep(destination_hash)} from storage") # TODO: Remove
+ ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
+ hexhash = RNS.hexrep(destination_hash, delimit=False)
+ ratchet_path = f"{ratchetdir}/hexhash"
+ if os.path.isfile(ratchet_path):
+ try:
+ ratchet_file = open(ratchet_path, "rb")
+ ratchet_data = umsgpack.unpackb(ratchets_file.read())
+ if time.time() < ratchet_data["received"]+Identity.RATCHET_EXPIRY and len(ratchet_data["ratchet"]) == Identity.RATCHETSIZE//8:
+ Identity.known_ratchets[destination_hash] = ratchet_data["ratchet"]
+ else:
+ return None
+
+ except Exception as e:
+ RNS.log(f"An error occurred while loading ratchet data for {RNS.prettyhexrep(destination_hash)} from storage.", RNS.LOG_ERROR)
+ RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
+ return None
+
+ if destination_hash in Identity.known_ratchets:
+ return Identity.known_ratchets[destination_hash]
+ else:
+ return None
@staticmethod
def validate_announce(packet, only_validate_signature=False):
try:
if packet.packet_type == RNS.Packet.ANNOUNCE:
+ keysize = Identity.KEYSIZE//8
+ ratchetsize = Identity.RATCHETSIZE//8
+ name_hash_len = Identity.NAME_HASH_LENGTH//8
+ sig_len = Identity.SIGLENGTH//8
destination_hash = packet.destination_hash
- public_key = packet.data[:Identity.KEYSIZE//8]
- name_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8]
- random_hash = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10]
- signature = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8]
- app_data = b""
- if len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
- app_data = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:]
- signed_data = destination_hash+public_key+name_hash+random_hash+app_data
+ # Get public key bytes from announce
+ public_key = packet.data[:keysize]
+
+ # If the packet context flag is set,
+ # this announce contains a new ratchet
+ if packet.context_flag == RNS.Packet.FLAG_SET:
+ name_hash = packet.data[keysize:keysize+name_hash_len ]
+ random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
+ ratchet = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+ratchetsize]
+ signature = packet.data[keysize+name_hash_len+10+ratchetsize:keysize+name_hash_len+10+ratchetsize+sig_len]
+ app_data = b""
+ if len(packet.data) > keysize+name_hash_len+10+sig_len+ratchetsize:
+ app_data = packet.data[keysize+name_hash_len+10+sig_len+ratchetsize:]
+
+ # If the packet context flag is not set,
+ # this announce does not contain a ratchet
+ else:
+ ratchet = b""
+ name_hash = packet.data[keysize:keysize+name_hash_len]
+ random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
+ signature = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+sig_len]
+ app_data = b""
+ if len(packet.data) > keysize+name_hash_len+10+sig_len:
+ app_data = packet.data[keysize+name_hash_len+10+sig_len:]
+
+ signed_data = destination_hash+public_key+name_hash+random_hash+ratchet+app_data
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
app_data = None
@@ -291,6 +368,9 @@ class Identity:
else:
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME)
+ if ratchet:
+ Identity._remember_ratchet(destination_hash, ratchet)
+
return True
else:
@@ -479,7 +559,7 @@ class Identity:
def get_context(self):
return None
- def encrypt(self, plaintext):
+ def encrypt(self, plaintext, ratchet=None):
"""
Encrypts information for the identity.
@@ -491,7 +571,13 @@ class Identity:
ephemeral_key = X25519PrivateKey.generate()
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes()
- shared_key = ephemeral_key.exchange(self.pub)
+ if ratchet != None:
+ RNS.log(f"Encrypting with ratchet {RNS.hexrep(ratchet)}", RNS.LOG_DEBUG) # TODO: Remove
+ target_public_key = X25519PublicKey.from_public_bytes(ratchet)
+ else:
+ target_public_key = self.pub
+
+ shared_key = ephemeral_key.exchange(target_public_key)
derived_key = RNS.Cryptography.hkdf(
length=32,
@@ -509,7 +595,7 @@ class Identity:
raise KeyError("Encryption failed because identity does not hold a public key")
- def decrypt(self, ciphertext_token):
+ def decrypt(self, ciphertext_token, ratchets=None):
"""
Decrypts information for the identity.
@@ -523,19 +609,40 @@ class Identity:
try:
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
-
- shared_key = self.prv.exchange(peer_pub)
-
- derived_key = RNS.Cryptography.hkdf(
- length=32,
- derive_from=shared_key,
- salt=self.get_salt(),
- context=self.get_context(),
- )
-
- fernet = Fernet(derived_key)
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
- plaintext = fernet.decrypt(ciphertext)
+
+ if ratchets:
+ for ratchet in ratchets:
+ try:
+ ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
+ shared_key = ratchet_prv.exchange(peer_pub)
+ derived_key = RNS.Cryptography.hkdf(
+ length=32,
+ derive_from=shared_key,
+ salt=self.get_salt(),
+ context=self.get_context(),
+ )
+
+ fernet = Fernet(derived_key)
+ plaintext = fernet.decrypt(ciphertext)
+ RNS.log(f"Decrypted with ratchet {RNS.hexrep(ratchet_prv.public_key().public_bytes())}", RNS.LOG_DEBUG) # TODO: Remove
+ break
+
+ except Exception as e:
+ pass
+ # RNS.log("Decryption using this ratchet failed", RNS.LOG_DEBUG) # TODO: Remove
+
+ if plaintext == None:
+ shared_key = self.prv.exchange(peer_pub)
+ derived_key = RNS.Cryptography.hkdf(
+ length=32,
+ derive_from=shared_key,
+ salt=self.get_salt(),
+ context=self.get_context(),
+ )
+
+ fernet = Fernet(derived_key)
+ plaintext = fernet.decrypt(ciphertext)
except Exception as e:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)

diff --git a/RNS/Link.py b/RNS/Link.py
index 47570171..95fdb123 100644
--- a/RNS/Link.py
+++ b/RNS/Link.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal

diff --git a/RNS/Packet.py b/RNS/Packet.py
index e47f05b4..4246aca9 100755
--- a/RNS/Packet.py
+++ b/RNS/Packet.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -83,6 +83,10 @@ class Packet:
LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof
+ # Context flag values
+ FLAG_SET = 0x01
+ FLAG_UNSET = 0x00
+
# This is used to calculate allowable
# payload sizes
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
@@ -102,7 +106,9 @@ class Packet:
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
- def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
+ def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
+ header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
+
if destination != None:
if transport_type == None:
transport_type = RNS.Transport.BROADCAST
@@ -111,6 +117,7 @@ class Packet:
self.packet_type = packet_type
self.transport_type = transport_type
self.context = context
+ self.context_flag = context_flag
self.hops = 0;
self.destination = destination
@@ -142,9 +149,10 @@ class Packet:
def get_packed_flags(self):
if self.context == Packet.LRPROOF:
- packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
+ packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
else:
- packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
+ packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
+
return packed_flags
def pack(self):
@@ -216,7 +224,8 @@ class Packet:
self.hops = self.raw[1]
self.header_type = (self.flags & 0b01000000) >> 6
- self.transport_type = (self.flags & 0b00110000) >> 4
+ self.context_flag = (self.flags & 0b00100000) >> 5
+ self.transport_type = (self.flags & 0b00010000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011)

diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py
index a681a986..d28ca082 100755
--- a/RNS/Reticulum.py
+++ b/RNS/Reticulum.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal

diff --git a/RNS/Transport.py b/RNS/Transport.py
index f3604602..a8d978be 100755
--- a/RNS/Transport.py
+++ b/RNS/Transport.py
@@ -1,6 +1,6 @@
# MIT License
#
-# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
+# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -403,7 +403,8 @@ class Transport:
header_type = RNS.Packet.HEADER_2,
transport_type = Transport.TRANSPORT,
transport_id = Transport.identity.hash,
- attached_interface = attached_interface
+ attached_interface = attached_interface,
+ context_flag = packet.context_flag,
)
new_packet.hops = announce_entry[4]

Served by rngit 1.5.0 - Generated in 0.11s